"""\
JScrapbook.py
"""

import sys
import traceback
import tokenize, keyword
import StringIO
from code import compile_command

import java, javax
from java import awt
from java.awt import BorderLayout
from javax import swing
from javax.swing.event import DocumentListener

class JScrapbook(swing.JPanel):
    def __init__(self, locals={}, fullColoringLimit=sys.maxint, *args, **kwargs):
        swing.JPanel.__init__(self, *args, **kwargs)
        self.diff_func = None
        self.locals = locals
        self.input = JScrapbookInput(self, fullColoringLimit=fullColoringLimit)
        self.output = JScrapbookOutput()

        self.split_pane = swing.JSplitPane(swing.JSplitPane.HORIZONTAL_SPLIT)
        self.split_pane.setLeftComponent(self.input)
        self.split_pane.setRightComponent(self.output)
        self.input.setMinimumSize(java.awt.Dimension(100, 100))
        self.output.setMinimumSize(java.awt.Dimension(100, 100))

        self.setLayout(BorderLayout())
        self.add(self.split_pane, BorderLayout.CENTER)

        header = "Jython %(version)s on %(platform)s\n\n\n" % {'version':sys.version, 'platform':sys.platform }
        self.output.print_to_stdout(header)

    def show_in_frame(self, size=None, exitOnClose=0):
        def close_frame(event):
            w = event.getWindow()
            w.dispose()
        def exit_app(event):
            sys.exit(0)

        if exitOnClose:
            close_func = exit_app
        else:
            close_func = close_frame

        self.frame = swing.JFrame("JScrapbook", windowClosing=close_func)
        self.frame.contentPane.add(self)
        if size is not None:
            self.frame.setSize(apply(java.awt.Dimension, size))
        else:
            self.frame.pack()
        self.frame.setVisible(1)

    def diff_dicts(self, loc_old, loc_new):
        loc_del = []
        for k, v in loc_old.items():
            if loc_new.has_key(k):
                if id(loc_new[k]) == id(loc_old[k]):
                    del loc_new[k]
            else:
                loc_del.append(k)
        return loc_del, loc_new

    def eval_code(self, expr):
        loc_old = self.locals.copy()
        self.eval_code_nodiff(expr)
        loc_new = self.locals.copy()
        loc_del, loc_diff = self.diff_dicts(loc_old, loc_new)
        if self.diff_func:
            self.diff_func(loc_del, loc_diff)

    def eval_code_nodiff(self, expr):
        lines = expr.split('\n')
        buf = []
        code_objects = []
        while len(lines) > 0:
            line = lines.pop(0)
            buf.append(line)
            try:
                clines = "\n".join(buf)
                c = compile_command(clines)
            except SyntaxError:
                traceback.print_exc(0,file=self.output)
                return
            if c:
                buf = []
                self.exec_code(c)
            else:
                continue

    def exec_code(self, c):
        try:
            std_streams = sys.stdout, sys.stderr
            sys.stdout = StringIO.StringIO()
            sys.stderr = StringIO.StringIO()
            try:
                exec c in self.locals
            finally:
                text_out = sys.stdout.getvalue()
                text_err = sys.stderr.getvalue()
                sys.stdout, sys.stderr = std_streams
                self.output.print_to_stdout(text_out)
                self.output.print_to_stderr(text_err)
        #Include these lines to actually exit on a sys.exit() call
        #except SystemExit, value:
        #    raise SystemExit, value
        except:
            exc_type, exc_value, exc_traceback = sys.exc_info()
            l = len(traceback.extract_tb(sys.exc_traceback))
            try:
                1/0
            except:
                m = len(traceback.extract_tb(sys.exc_traceback))
            type, value, tb = sys.exc_info()
            sys.last_type = exc_type
            sys.last_value = exc_value
            sys.last_traceback = exc_traceback
            tblist = traceback.extract_tb(exc_traceback)
            del tblist[:1]
            list = traceback.format_list(tblist)
            if list:
                list.insert(0, "Traceback (most recent call last):\n")
            list[len(list):] = traceback.format_exception_only(exc_type, exc_value)
            map(self.output.print_to_stderr, list)
            return

    def append_to_input(self, text):
        self.input.append(text)

class JScrapbookInput(swing.JScrollPane, DocumentListener, java.lang.Runnable):

    def __init__(self, scrapbook, locals={}, fullColoringLimit=sys.maxint, *args, **kwargs):
        swing.JScrollPane.__init__(self, *args, **kwargs)
        self.fullColoringLimit = fullColoringLimit
        self.text_pane = swing.JTextPane(keyPressed=self.OnKeyPressed)
        self.model = self.text_pane.getStyledDocument()
        self.psc = PySyntaxColorizer(self.model)
        self.scrapbook = scrapbook
        self.doc_event_pending = None
        self.set_styles()
        self.model.addDocumentListener(self)

        self.setViewportView(self.text_pane)

    def changedUpdate(self, event):
        pass

    def insertUpdate(self, event):
        self.post_colorize(event)

    def removeUpdate(self, event):
        self.post_colorize(event)

    def run(self):
    ##colorizing is temporariliy disabled ##################
        if self.doc_event_pending:
            self.colorize(self.doc_event_pending)
        pass

    # Setting styles of a JTextPane while handling events is not allowed,
    # therefore we postpone the colorization using invokeLater.
    def post_colorize(self, event):
        self.doc_event_pending = event
        swing.SwingUtilities.invokeLater(self) # call run method asynchronously

    def colorize(self, event):
        document = event.getDocument()
        start_pos = document.getStartPosition().getOffset()
        end_pos = document.getEndPosition().getOffset()
        text = document.getText(start_pos, end_pos - start_pos)
        if len(text) > self.fullColoringLimit:
            # restrict syntax coloring to surrounding lines because
            # the buffer is too large and coloring the whole buffer is too slow.
            # Multiline strings might be incorrectly colored.
            epos, elength, etype = event.getOffset(), event.getLength(), str(event.getType())
            if str(etype) == "INSERT":
                e_start = epos
                e_end = epos + elength
            elif etype == "REMOVE":
                e_start = epos - elength
                if e_start<0: e_start=0
                e_end = epos
            start_pos, end_pos = self.surrounding_lines(text, e_start, e_end)
            text = text[start_pos:end_pos]
        self.psc.colorize(text, start_pos)

    def surrounding_lines(self, text, off_start, off_end):
        previous_nl = text.rfind("\n", 0, off_start)
        next_nl = text.find("\n", off_end)
        if previous_nl == -1:
            previous_nl = 0
        if next_nl == -1:
            next_nl = len(text)
        return previous_nl, next_nl

    def set_styles(self):
        model = self.model
        ds = swing.text.StyleContext.getDefaultStyleContext().getStyle(swing.text.StyleContext.DEFAULT_STYLE)
        for stylename in 'normal', 'comment', 'keyword', 'string', 'number', 'class', 'method', 'comment_block':
            self.text_pane.addStyle(stylename, ds)

        style_normal = model.getStyle("normal")
        style_comment = model.getStyle("comment")
        style_keyword = model.getStyle("keyword")
        style_string = model.getStyle("string")
        style_number = model.getStyle("number")
        style_class = model.getStyle("class")
        style_method = model.getStyle("method")
        style_comment_block = model.getStyle("comment_block")

        # define our set of colors
        black = java.awt.Color.black
        dark_green = awt.Color(0, 128, 0)
        dark_blue = awt.Color(0, 0, 128)
        olive = awt.Color(128, 128, 0)
        cyan = awt.Color(0, 128, 128)
        blue = awt.Color(0, 0, 255)
        grey = awt.Color(128, 128, 128)

        # swing.text.StyleConstants.setFontFamily(ds, "SansSerif")
        swing.text.StyleConstants.setForeground(style_normal, black)
        swing.text.StyleConstants.setForeground(style_comment, dark_green)
        swing.text.StyleConstants.setForeground(style_keyword, dark_blue)
        swing.text.StyleConstants.setBold(style_keyword, 1)
        swing.text.StyleConstants.setForeground(style_string, olive)
        swing.text.StyleConstants.setForeground(style_number, cyan)
        swing.text.StyleConstants.setForeground(style_class, blue)
        swing.text.StyleConstants.setBold(style_class, 1)
        swing.text.StyleConstants.setForeground(style_method, cyan)
        swing.text.StyleConstants.setBold(style_method, 1)
        swing.text.StyleConstants.setForeground(style_comment_block, grey)

    def append(self, text):
        model = self.model
        end_offset = model.getEndPosition().getOffset() - 1
        model.insertString(end_offset, text, None)
        self.text_pane.setCaretPosition(model.getEndPosition().getOffset() - 1)

    def OnKeyPressed(self, event):
        if event.getKeyCode() == event.VK_ENTER:
            if event.getModifiers() == event.SHIFT_MASK:
                # run selection
                start_pos = self.text_pane.getSelectionStart()
                end_pos = self.text_pane.getSelectionEnd()
                if start_pos != end_pos:
                    text = self.model.getText(start_pos, end_pos - start_pos)
                    self.scrapbook.eval_code(text)
            elif event.getModifiers() == ( event.CTRL_MASK | event.SHIFT_MASK ):
                # run buffer
                start_pos = self.model.getStartPosition().getOffset()
                end_pos = self.model.getEndPosition().getOffset()
                text = self.model.getText(start_pos, end_pos - start_pos)
                self.scrapbook.eval_code(text)
            elif event.getModifiers() == event.CTRL_MASK:
                # run current line and append newline
                start_pos = self.model.getStartPosition().getOffset()
                end_pos = self.model.getEndPosition().getOffset()
                text = self.model.getText(start_pos, end_pos - start_pos)
                caret_pos = self.text_pane.getCaretPosition()
                line_start, line_end = self.surrounding_lines(text, caret_pos, caret_pos)
                text = text[line_start:line_end]
                text = text.strip()
                self.model.insertString(caret_pos, '\n', self.model.getStyle("normal"))
                self.scrapbook.eval_code(text)

class PySyntaxColorizer:

    def __init__(self, styled_doc):
        self.styled_doc = styled_doc
        self.token2stylename = {
          'COMMENT': 'comment',
          'NUMBER': 'number',
          'STRING': 'string',
          'NAME': 'normal',
          'OP': 'normal',
          'keyword': 'keyword',
          'method': 'method',
          'class': 'class',
          'comment_block': 'comment_block'
          }
        self.styles = {}

    def colorize(self, text, total_offset=0):
        self.total_offset = total_offset
        self.last_type = None
        self.last_token = None
        self.lines = text.split("\n")
        offset = 0
        self.line_offsets = []
        for l in self.lines:
            self.line_offsets.append(offset)
            offset += len(l) + 1 # count line + newline character
        try:
            tokenize.tokenize(self.nextline, self.handle_token)
        except: # a closer look at the errors might be helpful.
            pass

    def handle_token(self, type, token, (srow, scol), (erow, ecol), line):
        # print "%d,%d-%d,%d:\t%s\t%s" % (srow, scol, erow, ecol, tokenize.tok_name[type], repr(token))
        type_name = tokenize.tok_name[type]
        if type_name == 'COMMENT' and token.startswith('##'):
            type_name = 'comment_block'
        if type_name == 'NAME' and keyword.iskeyword(token):
            type_name = 'keyword'
            self.last_type = type_name
            self.last_token = token
        elif type_name == 'NAME' and self.last_type == 'keyword':
            if self.last_token == 'class':
                type_name = 'class'
            elif self.last_token == 'def':
                type_name = 'method'

        self.last_type, self.last_token = type_name, token
        if self.token2stylename.has_key(type_name):
            style = self.token2stylename[type_name]
            text_start = self.line_offsets[srow - 1] + scol
            text_end = self.line_offsets[erow - 1] + ecol
            text_len = text_end - text_start
            if not self.styles.has_key(style):
                self.styles[style] = self.styled_doc.getStyle(style)
            self.styled_doc.setCharacterAttributes(text_start + self.total_offset,
                                                   text_len, self.styles[style], 1)

    def nextline(self):
        if self.lines is not None:
            return self.lines.pop(0) + "\n"
        else:
            return None

class JScrapbookOutput(swing.JScrollPane):
    def __init__(self, *args, **kwargs):
        swing.JScrollPane.__init__(self, *args, **kwargs)
        self.text_pane = swing.JTextPane()

        ds = swing.text.StyleContext.getDefaultStyleContext().getStyle(swing.text.StyleContext.DEFAULT_STYLE)

        self.text_pane.addStyle("stdout", ds)
        self.text_pane.addStyle("stderr", ds)

        model = self.text_pane.getStyledDocument()
        stdout_style = model.getStyle("stdout")
        stderr_style = model.getStyle("stderr")

        swing.text.StyleConstants.setForeground(stdout_style, java.awt.Color.blue)
        swing.text.StyleConstants.setForeground(stderr_style, java.awt.Color.red)

        self.setViewportView(self.text_pane)

    def write(self, text):
        self.print_to_stderr(text)

    def print_to_stdout(self, text, style='stdout'):
        if text:
            model = self.text_pane.getStyledDocument()
            end_offset = model.getEndPosition().getOffset() - 1
            model.insertString(end_offset, text, model.getStyle(style))
            self.text_pane.setCaretPosition(model.getEndPosition().getOffset() - 1)

    def print_to_stderr(self, text):
        if text:
            self.print_to_stdout(text, style='stderr')

def run_shell():
    swing.UIManager.setLookAndFeel(swing.UIManager.getSystemLookAndFeelClassName())

    import __main__
    my_locals = { 'sys': sys, '__main__': __main__ }
    jsb = JScrapbook(locals=my_locals, fullColoringLimit=400)
    jsb.show_in_frame(size=(300, 500), exitOnClose=1)
    jsb.eval_code("print >>sys.stderr, 'Hello stderr'")
    jsb.eval_code("print 'Hello stdout'")
    __main__.jsb = jsb

if __name__ == '__main__':
    run_shell()
